diff --git a/web/media/media.css b/web/media/media.css index c1103aaea..60a95260b 100644 --- a/web/media/media.css +++ b/web/media/media.css @@ -1,138 +1,87 @@ span.clickable { cursor: pointer; } span.multimedia { display: inline-flex; align-items: center; justify-content: center; position: relative; vertical-align: top; } span.multimedia > .multimediaImage { position: relative; min-height: 50px; min-width: 50px; } span.multimedia > .multimediaImage > img, span.multimedia > .multimediaImage > video { /* this should be in sync with the MAX_THUMBNAIL_HEIGHT */ /* in multimedia.react.js */ max-height: 200px; max-width: 100%; display: inherit; } /* Prevent borders around images without src attribute */ span.multimedia > .multimediaImage > img:not([src]) { visibility: hidden; } span.multimedia > .multimediaImage svg.removeUpload { display: none; position: absolute; top: 3px; right: 3px; color: white; border-radius: 50%; box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.5); background-color: rgba(34, 34, 34, 0.67); } span.multimedia:hover > .multimediaImage svg.removeUpload { display: inherit; } span.multimedia svg.uploadError { position: absolute; top: 0; bottom: 0; left: 0; right: 0; margin: auto auto; color: white; border-radius: 50%; box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.5); background-color: #dd2222; } span.multimedia > svg.progressIndicator { position: absolute; top: 0; bottom: 0; left: 0; right: 0; margin: auto auto; width: 50px; height: 50px; } -span.multimedia .loadingIndicator, -div.multimediaModalOverlay .loadingIndicator { +span.multimedia .loadingIndicator { position: absolute; top: 0; bottom: 0; left: 0; right: 0; margin: auto auto; width: 25px; height: 25px; } :global(.CircularProgressbar-background) { fill: #666 !important; } :global(.CircularProgressbar-text) { fill: #fff !important; } :global(.CircularProgressbar-path) { stroke: #fff !important; } :global(.CircularProgressbar-trail) { stroke: transparent !important; } - -div.multimediaModalOverlay { - position: fixed; - left: 0; - top: 0; - z-index: 4; - width: 100%; - height: 100%; - background-color: rgba(0, 0, 0, 0.9); - overflow: auto; - padding: 10px; - box-sizing: border-box; - display: flex; - justify-content: center; - -webkit-app-region: no-drag; -} -div.multimediaModalOverlay > .mediaContainer { - display: flex; - justify-content: center; - align-items: center; - width: 100%; - height: 100%; -} -div.multimediaModalOverlay > .mediaContainer:focus { - outline: none; -} -div.mediaContainer > img, -div.mediaContainer > video { - width: auto; - height: auto; - max-width: 100%; - max-height: 100%; - display: block; - margin: auto; - background-position: center; - background-size: cover; - background-repeat: no-repeat; -} -svg.closeMultimediaModal { - position: absolute; - cursor: pointer; - top: 15px; - right: 15px; - color: white; - border-radius: 50%; - box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.5); - background-color: rgba(34, 34, 34, 0.67); - height: 36px; - width: 36px; -} diff --git a/web/media/multimedia-modal.react.js b/web/media/multimedia-modal.react.js index 1da7f9f8d..ca88ee1be 100644 --- a/web/media/multimedia-modal.react.js +++ b/web/media/multimedia-modal.react.js @@ -1,208 +1,39 @@ // @flow -import invariant from 'invariant'; import * as React from 'react'; -import { XCircle as XCircleIcon } from 'react-feather'; -import { useModalContext } from 'lib/components/modal-provider.react.js'; -import { fetchableMediaURI } from 'lib/media/media-utils.js'; import type { EncryptedMediaType, MediaType, Dimensions, } from 'lib/types/media-types.js'; -import EncryptedMultimedia from './encrypted-multimedia.react.js'; -import LoadableVideo from './loadable-video.react.js'; -import { usePlaceholder } from './media-utils.js'; -import css from './media.css'; +import FullScreenViewModal from '../modals/full-screen-view-modal.react.js'; type MediaInfo = | { +type: MediaType, +uri: string, +dimensions: ?Dimensions, +thumbHash: ?string, +thumbnailURI: ?string, } | { +type: EncryptedMediaType, +blobURI: string, +encryptionKey: string, +dimensions: ?Dimensions, +thumbHash: ?string, +thumbnailBlobURI: ?string, +thumbnailEncryptionKey: ?string, }; -type BaseProps = { - +media: MediaInfo, -}; - type Props = { - ...BaseProps, - +popModal: (modal: ?React.Node) => void, - +placeholderImage: ?string, -}; - -type State = { - +dimensions: ?Dimensions, + +media: MediaInfo, }; -class MultimediaModal extends React.PureComponent { - overlay: ?HTMLDivElement; - - constructor(props: Props) { - super(props); - this.state = { dimensions: null }; - } - - componentDidMount() { - invariant(this.overlay, 'overlay ref unset'); - this.overlay.focus(); - this.calculateMediaDimensions(); - window.addEventListener('resize', this.calculateMediaDimensions); - } - - componentWillUnmount() { - window.removeEventListener('resize', this.calculateMediaDimensions); - } - - render(): React.Node { - let mediaModalItem; - const { media, placeholderImage } = this.props; - const style = { - backgroundImage: placeholderImage - ? `url(${placeholderImage})` - : undefined, - }; - if (media.type === 'photo') { - const uri = fetchableMediaURI(media.uri); - mediaModalItem = ; - } else if (media.type === 'video') { - const uri = fetchableMediaURI(media.uri); - const { thumbnailURI } = media; - invariant(thumbnailURI, 'video missing thumbnail'); - mediaModalItem = ( - - ); - } else { - invariant( - media.type === 'encrypted_photo' || media.type === 'encrypted_video', - 'invalid media type', - ); - const { - type, - blobURI, - encryptionKey, - thumbnailBlobURI, - thumbnailEncryptionKey, - } = media; - const dimensions = this.state.dimensions ?? media.dimensions; - const elementStyle = dimensions - ? { - width: `${dimensions.width}px`, - height: `${dimensions.height}px`, - } - : undefined; - mediaModalItem = ( - - ); - } - - return ( -
-
- {mediaModalItem} -
- -
- ); - } - - overlayRef: (overlay: ?HTMLDivElement) => void = overlay => { - this.overlay = overlay; - }; - - onBackgroundClick: (event: SyntheticEvent) => void = - event => { - if (event.target === this.overlay) { - this.props.popModal(); - } - }; - - onKeyDown: (event: SyntheticKeyboardEvent) => void = - event => { - if (event.key === 'Escape') { - this.props.popModal(); - } - }; - - calculateMediaDimensions: () => void = () => { - if (!this.overlay || !this.props.media.dimensions) { - return; - } - const containerWidth = this.overlay.clientWidth; - const containerHeight = this.overlay.clientHeight; - const containerAspectRatio = containerWidth / containerHeight; - - const { width: mediaWidth, height: mediaHeight } = - this.props.media.dimensions; - const mediaAspectRatio = mediaWidth / mediaHeight; - - let newWidth, newHeight; - if (containerAspectRatio > mediaAspectRatio) { - newWidth = Math.min(mediaWidth, containerHeight * mediaAspectRatio); - newHeight = newWidth / mediaAspectRatio; - } else { - newHeight = Math.min(mediaHeight, containerWidth / mediaAspectRatio); - newWidth = newHeight * mediaAspectRatio; - } - this.setState({ - dimensions: { - width: newWidth, - height: newHeight, - }, - }); - }; -} - -function ConnectedMultiMediaModal(props: BaseProps): React.Node { - const modalContext = useModalContext(); - const { thumbHash, encryptionKey, thumbnailEncryptionKey } = props.media; - const thumbHashEncryptionKey = thumbnailEncryptionKey ?? encryptionKey; - const placeholderImage = usePlaceholder(thumbHash, thumbHashEncryptionKey); - - return ( - - ); +function MultimediaModal(props: Props): React.Node { + return ; } -export default ConnectedMultiMediaModal; +export default MultimediaModal; diff --git a/web/modals/full-screen-view-modal.css b/web/modals/full-screen-view-modal.css new file mode 100644 index 000000000..dd96a1994 --- /dev/null +++ b/web/modals/full-screen-view-modal.css @@ -0,0 +1,60 @@ +div.multimediaModalOverlay .loadingIndicator { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + margin: auto auto; + width: 25px; + height: 25px; +} + +div.multimediaModalOverlay { + position: fixed; + left: 0; + top: 0; + z-index: 4; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.9); + overflow: auto; + padding: 10px; + box-sizing: border-box; + display: flex; + justify-content: center; + -webkit-app-region: no-drag; +} +div.multimediaModalOverlay > .mediaContainer { + display: flex; + justify-content: center; + align-items: center; + width: 100%; + height: 100%; +} +div.multimediaModalOverlay > .mediaContainer:focus { + outline: none; +} +div.mediaContainer > img, +div.mediaContainer > video { + width: auto; + height: auto; + max-width: 100%; + max-height: 100%; + display: block; + margin: auto; + background-position: center; + background-size: cover; + background-repeat: no-repeat; +} +svg.closeMultimediaModal { + position: absolute; + cursor: pointer; + top: 15px; + right: 15px; + color: white; + border-radius: 50%; + box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.5); + background-color: rgba(34, 34, 34, 0.67); + height: 36px; + width: 36px; +} diff --git a/web/media/multimedia-modal.react.js b/web/modals/full-screen-view-modal.react.js similarity index 95% copy from web/media/multimedia-modal.react.js copy to web/modals/full-screen-view-modal.react.js index 1da7f9f8d..3aca565db 100644 --- a/web/media/multimedia-modal.react.js +++ b/web/modals/full-screen-view-modal.react.js @@ -1,208 +1,208 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { XCircle as XCircleIcon } from 'react-feather'; import { useModalContext } from 'lib/components/modal-provider.react.js'; import { fetchableMediaURI } from 'lib/media/media-utils.js'; import type { EncryptedMediaType, MediaType, Dimensions, } from 'lib/types/media-types.js'; -import EncryptedMultimedia from './encrypted-multimedia.react.js'; -import LoadableVideo from './loadable-video.react.js'; -import { usePlaceholder } from './media-utils.js'; -import css from './media.css'; +import css from './full-screen-view-modal.css'; +import EncryptedMultimedia from '../media/encrypted-multimedia.react.js'; +import LoadableVideo from '../media/loadable-video.react.js'; +import { usePlaceholder } from '../media/media-utils.js'; type MediaInfo = | { +type: MediaType, +uri: string, +dimensions: ?Dimensions, +thumbHash: ?string, +thumbnailURI: ?string, } | { +type: EncryptedMediaType, +blobURI: string, +encryptionKey: string, +dimensions: ?Dimensions, +thumbHash: ?string, +thumbnailBlobURI: ?string, +thumbnailEncryptionKey: ?string, }; type BaseProps = { +media: MediaInfo, }; type Props = { ...BaseProps, +popModal: (modal: ?React.Node) => void, +placeholderImage: ?string, }; type State = { +dimensions: ?Dimensions, }; class MultimediaModal extends React.PureComponent { overlay: ?HTMLDivElement; constructor(props: Props) { super(props); this.state = { dimensions: null }; } componentDidMount() { invariant(this.overlay, 'overlay ref unset'); this.overlay.focus(); this.calculateMediaDimensions(); window.addEventListener('resize', this.calculateMediaDimensions); } componentWillUnmount() { window.removeEventListener('resize', this.calculateMediaDimensions); } render(): React.Node { let mediaModalItem; const { media, placeholderImage } = this.props; const style = { backgroundImage: placeholderImage ? `url(${placeholderImage})` : undefined, }; if (media.type === 'photo') { const uri = fetchableMediaURI(media.uri); mediaModalItem = ; } else if (media.type === 'video') { const uri = fetchableMediaURI(media.uri); const { thumbnailURI } = media; invariant(thumbnailURI, 'video missing thumbnail'); mediaModalItem = ( ); } else { invariant( media.type === 'encrypted_photo' || media.type === 'encrypted_video', 'invalid media type', ); const { type, blobURI, encryptionKey, thumbnailBlobURI, thumbnailEncryptionKey, } = media; const dimensions = this.state.dimensions ?? media.dimensions; const elementStyle = dimensions ? { width: `${dimensions.width}px`, height: `${dimensions.height}px`, } : undefined; mediaModalItem = ( ); } return (
{mediaModalItem}
); } overlayRef: (overlay: ?HTMLDivElement) => void = overlay => { this.overlay = overlay; }; onBackgroundClick: (event: SyntheticEvent) => void = event => { if (event.target === this.overlay) { this.props.popModal(); } }; onKeyDown: (event: SyntheticKeyboardEvent) => void = event => { if (event.key === 'Escape') { this.props.popModal(); } }; calculateMediaDimensions: () => void = () => { if (!this.overlay || !this.props.media.dimensions) { return; } const containerWidth = this.overlay.clientWidth; const containerHeight = this.overlay.clientHeight; const containerAspectRatio = containerWidth / containerHeight; const { width: mediaWidth, height: mediaHeight } = this.props.media.dimensions; const mediaAspectRatio = mediaWidth / mediaHeight; let newWidth, newHeight; if (containerAspectRatio > mediaAspectRatio) { newWidth = Math.min(mediaWidth, containerHeight * mediaAspectRatio); newHeight = newWidth / mediaAspectRatio; } else { newHeight = Math.min(mediaHeight, containerWidth / mediaAspectRatio); newWidth = newHeight * mediaAspectRatio; } this.setState({ dimensions: { width: newWidth, height: newHeight, }, }); }; } function ConnectedMultiMediaModal(props: BaseProps): React.Node { const modalContext = useModalContext(); const { thumbHash, encryptionKey, thumbnailEncryptionKey } = props.media; const thumbHashEncryptionKey = thumbnailEncryptionKey ?? encryptionKey; const placeholderImage = usePlaceholder(thumbHash, thumbHashEncryptionKey); return ( ); } export default ConnectedMultiMediaModal;